﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Configuration.Provider;
using System.Security.Cryptography;
using System.Collections.Specialized;
using System.Web.Security;
using CoolCode.Entities;
using CoolCode.Services;

namespace CoolCode.Web.Security
{
    public class LinqMembershipProvider : MembershipProvider
    {
        #region Static Fields

        public static readonly string DefaultProviderName = "LinqMembershipProvider";
        public static readonly string DefaultProviderDescription = "Linq Membership Provider";

        #endregion

        #region Fields

        private string _applicationName;
        private bool _enablePasswordReset;
        private int _maxInvalidPasswordAttempts;
        private int _minRequiredNonAlphanumericCharacters;
        private int _minRequiredPasswordLength;
        private int _passwordAttemptWindow;
        private MembershipPasswordFormat _passwordFormat;
        private string _passwordStrengthRegularExpression;
        private bool _requiresQuestionAndAnswer;
        private bool _requiresUniqueEmail;

        #endregion

        #region Properties

        /// <summary>
        /// The name of the application using the custom membership provider.
        /// </summary>
        /// <value></value>
        /// <returns>The name of the application using the custom membership provider.</returns>
        public override string ApplicationName
        {
            get { return _applicationName; }
            set { _applicationName = value; }
        }

        /// <summary>
        /// Indicates whether the membership provider is configured to allow users to reset their passwords.
        /// </summary>
        /// <value></value>
        /// <returns>true if the membership provider supports password reset; otherwise, false. The default is true.</returns>
        public override bool EnablePasswordReset
        {
            get { return _enablePasswordReset; }
        }

        /// <summary>
        /// Indicates whether the membership provider is configured to allow users to retrieve their passwords.
        /// </summary>
        /// <value></value>
        /// <returns>true if the membership provider is configured to support password retrieval; otherwise, false. The default is false.</returns>
        public override bool EnablePasswordRetrieval
        {
            get { return _passwordFormat != MembershipPasswordFormat.Hashed; }
        }

        /// <summary>
        /// Gets the number of invalid password or password-answer attempts allowed before the membership user is locked out.
        /// </summary>
        /// <value></value>
        /// <returns>The number of invalid password or password-answer attempts allowed before the membership user is locked out.</returns>
        public override int MaxInvalidPasswordAttempts
        {
            get { return _maxInvalidPasswordAttempts; }
        }

        /// <summary>
        /// Gets the minimum number of special characters that must be present in a valid password.
        /// </summary>
        /// <value></value>
        /// <returns>The minimum number of special characters that must be present in a valid password.</returns>
        public override int MinRequiredNonAlphanumericCharacters
        {
            get { return _minRequiredNonAlphanumericCharacters; }
        }

        /// <summary>
        /// Gets the minimum length required for a password.
        /// </summary>
        /// <value></value>
        /// <returns>The minimum length required for a password. </returns>
        public override int MinRequiredPasswordLength
        {
            get { return _minRequiredPasswordLength; }
        }

        /// <summary>
        /// Gets the number of minutes in which a maximum number of invalid password or password-answer attempts are allowed before the membership user is locked out.
        /// </summary>
        /// <value></value>
        /// <returns>The number of minutes in which a maximum number of invalid password or password-answer attempts are allowed before the membership user is locked out.</returns>
        public override int PasswordAttemptWindow
        {
            get { return _passwordAttemptWindow; }
        }

        /// <summary>
        /// Gets a value indicating the format for storing passwords in the membership data store.
        /// </summary>
        /// <value></value>
        /// <returns>One of the <see cref="T:MembershipPasswordFormat"></see> values indicating the format for storing passwords in the data store.</returns>
        public override MembershipPasswordFormat PasswordFormat
        {
            get { return _passwordFormat; }
        }

        /// <summary>
        /// Gets the regular expression used to evaluate a password.
        /// </summary>
        /// <value></value>
        /// <returns>A regular expression used to evaluate a password.</returns>
        public override string PasswordStrengthRegularExpression
        {
            get { return _passwordStrengthRegularExpression; }
        }

        /// <summary>
        /// Gets a value indicating whether the membership provider is configured to require the user to answer a password question for password reset and retrieval.
        /// </summary>
        /// <value></value>
        /// <returns>true if a password answer is required for password reset and retrieval; otherwise, false. The default is true.</returns>
        public override bool RequiresQuestionAndAnswer
        {
            get { return _requiresQuestionAndAnswer; }
        }

        /// <summary>
        /// Gets a value indicating whether the membership provider is configured to require a unique e-mail address for each user name.
        /// </summary>
        /// <value></value>
        /// <returns>true if the membership provider requires a unique e-mail address; otherwise, false. The default is true.</returns>
        public override bool RequiresUniqueEmail
        {
            get { return _requiresUniqueEmail; }
        }

        #endregion

        #region Services

        private IMembershipService _service;

        protected virtual IMembershipService GetService()
        {
            if (_service == null)
            {
                _service =  ServiceProvider.Get();// ServiceFactory.Produce<IMembershipService>();
            }
            return _service;
        }

        #endregion

        public override bool ChangePassword(string username, string oldPassword, string newPassword)
        {
            try
            {
                var service = GetService();
                var user = service.GetUserByName(username);
                if (user == null)
                    throw new Exception("User does not exist!");

                if (ValidateUserInternal(user, oldPassword))
                {
                    var args = new ValidatePasswordEventArgs(username, newPassword, false);
                    base.OnValidatingPassword(args);
                    if (args.Cancel)
                    {
                        if (args.FailureInformation != null)
                            throw args.FailureInformation;
                        else
                            throw new MembershipPasswordException("Change password canceled due to new password validation failure.");
                    }
                    if (!ValidatePassword(newPassword))
                        throw new ArgumentException("Password doesn't meet password strength requirements!");
                    ///
                    string passwordSalt = string.Empty;
                    user.Password = TransformPassword(newPassword, ref passwordSalt);
                    user.PasswordSalt = passwordSalt;
                    user.LastPasswordChangedDate = DateTime.Now;
                    service.Submit();
                    ///
                    return true;
                }
                else
                    return false;
            }
            catch
            {
                throw;
            }
        }

        public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
        {
            try
            {
                var service = GetService();
                var user = service.GetUserByName(username);
                if (user != null && ValidateUserInternal(user, password))
                {
                    user.PasswordQuestion = newPasswordQuestion;
                    user.PasswordAnswer = newPasswordAnswer;
                    service.Submit();
                    return true;
                }
            }
            catch { throw; }
            //
            return false;
        }

        public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
        {
            status = MembershipCreateStatus.Success;
            try
            {
                var service = GetService();
                // Validate the username and email
                if (!service.ValidateUserName(username, Guid.Empty))
                {
                    status = MembershipCreateStatus.DuplicateUserName;
                    return null;
                }
                //Original //if (this.RequiresUniqueEmail && !service.ValidateEmail(password, Guid.Empty)) { 
                if (this.RequiresUniqueEmail && !service.ValidateEmail(email, Guid.Empty))
                {
                    status = MembershipCreateStatus.DuplicateEmail;
                    return null;
                }

                // Validate the password
                var e = new ValidatePasswordEventArgs(username, password, true);
                // Raise the event before validating the password
                base.OnValidatingPassword(e);
                if (e.Cancel || !ValidatePassword(password))
                {
                    status = MembershipCreateStatus.InvalidPassword;
                    return null;
                }

                var app = service.GetApplicationByName(this.ApplicationName ?? "/");
                if (app == null)
                {
                    app = new aspnet_Application
                    {
                        ApplicationId = Guid.NewGuid(),
                        ApplicationName = this.ApplicationName,
                        LoweredApplicationName = this.ApplicationName.ToLower(),
                        Description = null
                    };
                    service.SaveApplication(app);
                }

                aspnet_Membership user = new aspnet_Membership();
                user.ApplicationId = app.ApplicationId;
                user.UserId = Guid.NewGuid();
                user.aspnet_User = new aspnet_User
                {
                    UserId = user.UserId,
                    UserName = username,
                    LoweredUserName = username.ToLower(),
                    LastActivityDate = DateTime.Now,
                    ApplicationId = app.ApplicationId
                };

                this.TransformPassword(user, password);
                user.Email = email;
                if(!string.IsNullOrEmpty(email))
                user.LoweredEmail = email.ToLower();
                user.PasswordQuestion = passwordQuestion;
                user.PasswordAnswer = passwordAnswer;
                user.IsApproved = isApproved;
                user.CreateDate = DateTime.Now;
                user.LastLoginDate = DateTime.Now;
                user.LastPasswordChangedDate = DateTime.Now;
                user.LastLockoutDate = DateTime.Now;
                user.FailedPasswordAnswerAttemptCount = this.PasswordAttemptWindow ;
                user.FailedPasswordAnswerAttemptWindowStart = DateTime.Now;
                user.FailedPasswordAttemptCount = this.PasswordAttemptWindow;
                user.FailedPasswordAttemptWindowStart = DateTime.Now;
                service.SaveUser(user);

                return CreateMembershipFromInternalUser(user);
            }
            catch
            {
                throw;
            }
        }

        public override bool DeleteUser(string username, bool deleteAllRelatedData)
        {
            try
            {
                var service = GetService();
                return service.DeleteUser(username, deleteAllRelatedData);
            }
            catch
            {
                throw;
            }
        }

        public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            try
            {
                var service = GetService();
                var matchingUsers = service.PagingUserByEmail(emailToMatch, new PageParam(pageIndex, pageSize));
                totalRecords = matchingUsers.TotalRecords;
                return CreateMembershipCollectionFromInternalList(matchingUsers);
            }
            catch
            {
                throw;
            }
        }

        public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            try
            {
                var service = GetService();
                var matchingUsers = service.PagingUserByName(usernameToMatch, new PageParam(pageIndex, pageSize));
                totalRecords = matchingUsers.TotalRecords;
                return CreateMembershipCollectionFromInternalList(matchingUsers);
            }
            catch
            {
                throw;
            }
        }

        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
        {
            try
            {
                var service = GetService();
                var matchingUsers = service.PagingUser(new PageParam(pageIndex, pageSize));
                totalRecords = matchingUsers.TotalRecords;
                return CreateMembershipCollectionFromInternalList(matchingUsers);
            }
            catch
            {
                throw;
            }
        }

        public override int GetNumberOfUsersOnline()
        {
            var service = GetService();
            return service.GetNumberOfUsersOnline(DateTime.Now.AddMinutes(-Membership.UserIsOnlineTimeWindow));
        }

        public override string GetPassword(string username, string answer)
        {
            try
            {
                if (EnablePasswordRetrieval)
                {
                    var service = GetService();
                    var user = service.GetUserByName(username);
                    if (user != null)
                    {
                        // >> FIX http://www.codeplex.com/aspnetxmlproviders/WorkItem/View.aspx?WorkItemId=9701
                        if (RequiresQuestionAndAnswer && answer.Equals(user.PasswordAnswer, StringComparison.OrdinalIgnoreCase))
                        {
                            return UnEncodePassword(user.Password);
                        }
                        else if (!RequiresQuestionAndAnswer)
                        {
                            return UnEncodePassword(user.Password);
                        }
                        else
                        {
                            throw new MembershipPasswordException();
                        }
                        // << FIX
                    }
                }
                return null;
            }
            catch
            {
                throw;
            }
        }

        public override MembershipUser GetUser(string username, bool userIsOnline)
        {
            try
            {
                var service = GetService();
                var user = service.GetUserByName(username);
                if (user != null)
                {
                    if (userIsOnline)
                    {
                        user.aspnet_User.LastActivityDate = DateTime.Now;
                        service.Submit();
                    }
                    return CreateMembershipFromInternalUser(user);
                }
                else
                    return null;
            }
            catch { throw; }
        }

        public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
        {
            try
            {
                var service = GetService();
                var user = service.GetUserByKey((Guid)providerUserKey);
                if (user != null)
                {
                    if (userIsOnline)
                    {
                        user.aspnet_User.LastActivityDate = DateTime.Now;
                        service.Submit();
                    }
                    return CreateMembershipFromInternalUser(user);
                }
                else
                    return null;
            }
            catch { throw; }

        }

        public override string GetUserNameByEmail(string email)
        {
            try
            {
                var service = GetService();
                var user = service.GetUserByEmail(email);
                return user != null ? user.aspnet_User.UserName : null;
            }
            catch
            {
                throw;
            }
        }

        public override string ResetPassword(string username, string answer)
        {
            try
            {
                var service = GetService();
                var user = service.GetUserByName(username);
                if (user != null)
                {
                    if (answer != null && !user.PasswordAnswer.Equals(answer, StringComparison.OrdinalIgnoreCase))
                        throw new Exception("Invalid answer entered!");
                    else if (answer == null && Membership.RequiresQuestionAndAnswer)
                        throw new Exception("Invalid question and answer entered!");

                    try
                    {
                        byte[] NewPassword = new byte[16];
                        RandomNumberGenerator rng = RandomNumberGenerator.Create();
                        rng.GetBytes(NewPassword);

                        string NewPasswordString = Convert.ToBase64String(NewPassword);
                        this.TransformPassword(user, NewPasswordString);
                        service.Submit();

                        return NewPasswordString;
                    }
                    catch { throw; }
                }
                return null;
            }
            catch
            {
                throw;
            }
        }

        public override bool UnlockUser(string userName)
        {
            return true;
        }

        public override void UpdateUser(MembershipUser user)
        {
            try
            {
                var service = GetService();
                var suser = service.GetUserByKey((Guid)user.ProviderUserKey);
                if (suser != null)
                {
                    if (!service.ValidateUserName(suser.aspnet_User.UserName, suser.UserId))
                        throw new ArgumentException("UserName is not unique!");
                    if (this.RequiresUniqueEmail && !service.ValidateEmail(suser.Email, suser.UserId))
                        throw new ArgumentException("Email is not unique!");
                    suser.Email = user.Email;
                    suser.aspnet_User.LastActivityDate = user.LastActivityDate;
                    suser.LastLoginDate = user.LastLoginDate;
                    suser.Comment = user.Comment;
                    suser.IsApproved = user.IsApproved;
                    service.Submit();
                }
            }
            catch { throw; }
        }

        public override bool ValidateUser(string username, string password)
        {
            try
            {
                var service = GetService();
                var user = service.GetUserByName(username);
                if (user == null) return false;

                if (ValidateUserInternal(user, password))
                {
                    user.LastLoginDate = DateTime.Now;
                    user.aspnet_User.LastActivityDate = DateTime.Now;
                    service.Submit();
                    return true;
                }
                else
                    return false;
            }
            catch { throw; }
        }


        #region - Helpers -

        private void TransformPassword(aspnet_Membership user, string password)
        {
            string passwordSalt = string.Empty;
            user.Password = this.TransformPassword(password, ref passwordSalt);
            user.PasswordSalt = passwordSalt;
        }


        /// <summary>
        /// Transforms the password.
        /// </summary>
        /// <param name="password">The password.</param>
        /// <param name="salt">The salt.</param>
        /// <returns></returns>
        private string TransformPassword(string password, ref string salt)
        {
            string ret = string.Empty;
            switch (PasswordFormat)
            {
                case MembershipPasswordFormat.Clear:
                    ret = password;
                    break;
                case MembershipPasswordFormat.Hashed:
                    // Generate the salt if not passed in
                    if (string.IsNullOrEmpty(salt))
                    {
                        byte[] saltBytes = new byte[16];
                        RandomNumberGenerator rng = RandomNumberGenerator.Create();
                        rng.GetBytes(saltBytes);
                        salt = Convert.ToBase64String(saltBytes);
                    }
                    ret = FormsAuthentication.HashPasswordForStoringInConfigFile((salt + password), "SHA1");
                    break;
                case MembershipPasswordFormat.Encrypted:
                    byte[] clearText = Encoding.UTF8.GetBytes(password);
                    byte[] encryptedText = base.EncryptPassword(clearText);
                    ret = Convert.ToBase64String(encryptedText);
                    break;
            }
            return ret;
        }

        /// <summary>
        /// Validates the password.
        /// </summary>
        /// <param name="password">The password.</param>
        /// <returns></returns>
        private bool ValidatePassword(string password)
        {

            bool isValid = true;
            Regex regex;
            // Validate simple properties
            isValid = isValid && (password.Length >= this.MinRequiredPasswordLength);
            // Validate non-alphanumeric characters
            regex = new Regex(@"\W");
            isValid = isValid && (regex.Matches(password).Count >= this.MinRequiredNonAlphanumericCharacters);
            // Validate regular expression
            regex = new Regex(this.PasswordStrengthRegularExpression);
            isValid = isValid && (regex.Matches(password).Count > 0);
            ///
            return isValid;
        }

        /// <summary>
        /// Uns the encode password.
        /// </summary>
        /// <param name="encodedPassword">The encoded password.</param>
        /// <returns></returns>
        private string UnEncodePassword(string encodedPassword)
        {
            string password = encodedPassword;

            switch (PasswordFormat)
            {
                case MembershipPasswordFormat.Clear:
                    break;
                case MembershipPasswordFormat.Encrypted:
                    password =
                    Encoding.UTF8.GetString(DecryptPassword(Convert.FromBase64String(password)));
                    break;
                case MembershipPasswordFormat.Hashed:
                    throw new ProviderException("Cannot unencode a hashed password.");
                default:
                    throw new ProviderException("Unsupported password format.");
            }
            return password;
        }

        /// <summary>
        /// Validates the user internal.
        /// </summary>
        /// <param name="user">The user.</param>
        /// <param name="password">The password.</param>
        /// <returns></returns>
        private bool ValidateUserInternal(aspnet_Membership user, string password)
        {
            if (user != null)
            {
                string passwordSalt = user.PasswordSalt ;
                string passwordValidate = TransformPassword(password, ref passwordSalt); 
                if (string.Compare(passwordValidate, user.Password) == 0)
                {
                    return true;
                }
            }
            return false;
        }


        /// <summary>
        /// Creates the membership from internal user.
        /// </summary>
        /// <param name="user">The user.</param>
        /// <returns></returns>
        private MembershipUser CreateMembershipFromInternalUser(aspnet_Membership user)
        {
            MembershipUser muser = new MembershipUser(base.Name,
                user.aspnet_User.UserName, user.UserId, user.Email, user.PasswordQuestion,
                user.Comment, user.IsApproved, user.IsLockedOut, user.CreateDate, user.LastLoginDate,
                user.LastLoginDate, user.LastPasswordChangedDate, user.LastLockoutDate);

            return muser;
        }

        /// <summary>
        /// Creates the membership collection from internal list.
        /// </summary>
        /// <param name="users">The users.</param>
        /// <returns></returns>
        private MembershipUserCollection CreateMembershipCollectionFromInternalList(List<aspnet_Membership> users)
        {
            MembershipUserCollection returnCollection = new MembershipUserCollection();
            foreach (var user in users)
            {
                returnCollection.Add(CreateMembershipFromInternalUser(user));
            }
            return returnCollection;
        }

        #endregion

        #region - Initialize -

        /// <summary>
        /// Initializes the provider.
        /// </summary>
        /// <param name="name">The friendly name of the provider.</param>
        /// <param name="config">A collection of the name/value pairs representing the provider-specific attributes specified in the configuration for this provider.</param>
        /// <exception cref="T:System.ArgumentNullException">The name of the provider is null.</exception>
        /// <exception cref="T:System.InvalidOperationException">An attempt is made to call <see cref="M:System.Configuration.Provider.ProviderBase.Initialize(System.String,System.Collections.Specialized.NameValueCollection)"></see> on a provider after the provider has already been initialized.</exception>
        /// <exception cref="T:System.ArgumentException">The name of the provider has a length of zero.</exception>
        public override void Initialize(string name, NameValueCollection config)
        {
            if (config == null)
                throw new ArgumentNullException("config");
            if (string.IsNullOrEmpty(name))
            {
                name = DefaultProviderName;
            }
            if (string.IsNullOrEmpty(config["description"]))
            {
                config.Remove("description");
                config.Add("description", DefaultProviderDescription);
            }

            // Initialize the abstract base class.
            base.Initialize(name, config);

            // initialize fields
            _enablePasswordReset = Convert.ToBoolean(
               config.GetValue("enablePasswordReset", bool.TrueString));
            _maxInvalidPasswordAttempts = Convert.ToInt32(
               config.GetValue("maxInvalidPasswordAttempts", "5"));
            _minRequiredNonAlphanumericCharacters = Convert.ToInt32(
               config.GetValue("minRequiredNonAlphanumericCharacters", "0"));
            _minRequiredPasswordLength = Convert.ToInt32(
               config.GetValue("minRequiredPasswordLength", "5"));
            _passwordAttemptWindow = Convert.ToInt32(
               config.GetValue("passwordAttemptWindow", "10"));
            // >> FIX: http://www.codeplex.com/aspnetxmlproviders/WorkItem/View.aspx?WorkItemId=6743
            _passwordFormat = (MembershipPasswordFormat)Enum.Parse(
                typeof(MembershipPasswordFormat), config.GetValue("passwordFormat", "2"));
            // << FIX
            _passwordStrengthRegularExpression = Convert.ToString(
               config.GetValue("passwordStrengthRegularExpression", @"[\w| !?%&/()=\-?\*]*"));
            _requiresQuestionAndAnswer = Convert.ToBoolean(
               config.GetValue("requiresQuestionAndAnswer", bool.FalseString));
            _requiresUniqueEmail = Convert.ToBoolean(
               config.GetValue("requiresUniqueEmail", bool.TrueString));

            // initialize custom fields
            _applicationName = config.GetValue("applicationName", System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath);
            // << FIX

            ServiceProvider.Initial(config["connectionStringName"]);
            // starter User setup
            string initialRole = config["initialRole"];
            string initialUser = config["initialUser"];
            string initialPassword = config["initialPassword"];
            if (!string.IsNullOrEmpty(initialRole) && !string.IsNullOrEmpty(initialUser) && !string.IsNullOrEmpty(initialPassword))
            {
                SetupInitialUser(initialRole, initialUser, initialPassword);
            }
        }

        /// <summary>
        /// Setups the specified saved state.
        /// </summary>
        /// <param name="savedState">State of the saved.</param>
        static void SetupInitialUser(string role, string user, string password)
        {

            if (Roles.Enabled)
            {
                if (!Roles.RoleExists(role)) Roles.CreateRole(role);
                Membership.CreateUser(user, password);
                Roles.AddUserToRole(user, role);
            }
        }
        #endregion
  
    }
}
